/*
 * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.inspur.edp.web.frontendproject;

import com.fasterxml.jackson.databind.JsonNode;
import com.inspur.edp.lcm.metadata.api.context.PublishServiceContext;
import com.inspur.edp.lcm.metadata.api.context.PublishServiceContextHolder;
import com.inspur.edp.lcm.metadata.api.entity.FrameWorkTypeEnum;
import com.inspur.edp.lcm.metadata.api.entity.GspMetadata;
import com.inspur.edp.lcm.metadata.api.entity.MetadataCompilerContext;
import com.inspur.edp.lcm.metadata.api.entity.MetadataProperties;
import com.inspur.edp.lcm.metadata.spi.MetadataCompileAction;
import com.inspur.edp.web.common.customexception.WebCustomException;
import com.inspur.edp.web.common.entity.TerminalType;
import com.inspur.edp.web.common.environment.ExecuteEnvironment;
import com.inspur.edp.web.common.io.FileUtility;
import com.inspur.edp.web.common.logger.WebLogger;
import com.inspur.edp.web.common.metadata.MetadataGetterParameter;
import com.inspur.edp.web.common.metadata.MetadataTypeEnum;
import com.inspur.edp.web.common.metadata.MetadataUtility;
import com.inspur.edp.web.common.utility.ListUtility;
import com.inspur.edp.web.common.utility.StringUtility;
import com.inspur.edp.web.formmetadata.resolver.ResolveFormMetadataItem;
import com.inspur.edp.web.formmetadata.resolver.ResolveFormMetadataList;
import com.inspur.edp.web.frontendproject.changedetect.ChangeDetectExecuteManager;
import com.inspur.edp.web.frontendproject.changedetect.ChangeDetectExecuteResult;
import com.inspur.edp.web.frontendproject.changedetect.ChangeDetectExecuteType;
import com.inspur.edp.web.frontendproject.changedetect.context.ChangeDetectContext;
import com.inspur.edp.web.frontendproject.customservice.SourceServicePathGenerator;
import com.inspur.edp.web.frontendproject.entity.ChosenFormList;
import com.inspur.edp.web.frontendproject.generate.FrontendProjectGenerate;
import com.inspur.edp.web.frontendproject.metadata.FormMetadataManager;
import com.inspur.edp.web.frontendproject.resolver.FormMetadataResolver;
import com.inspur.edp.web.jitengine.JITEngineManager;
import com.inspur.edp.web.npmpackage.core.npminstall.global.NpmInstallGlobalChecker;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 前端工程编译器
 * 通过元数据的回调事件进行调用
 *
 * @author guozhiqi
 */
public class FrontendProjectCompiler implements MetadataCompileAction {
    /**
     * 工程编译扩展实现
     */
    @Override
    public void metadataCompile(MetadataCompilerContext context) {
        if (context == null || StringUtility.isNullOrEmpty(context.getProjectPath())) {
            WebLogger.Instance.error("the Project Path is Empty When Compiling a Project");
            throw new WebCustomException("the Project Path is Empty ");
        }
        WebLogger.Instance.info("begin compile frontend project", FrontendProjectCompiler.class.getName());
        generateAndCompileFrontendProject(context);

    }

    /**
     * 编译前端工程
     */
    private void generateAndCompileFrontendProject(MetadataCompilerContext context) {
        // 因为此处涉及到元数据的变更检测 因此不在乎此处进行route.json 文件的保存
        // 编译前端工程：面向PC设备
        if (FormMetadataManager.checkFormMetadataExists(context.getProjectPath(), TerminalType.PC, ChosenFormList.getNewInstance(), false, false)) {
            generateAndCompileFrontendProject(context.getProjectPath(), TerminalType.PC, false);
        }

        if (FormMetadataManager.checkFormMetadataExists(context.getProjectPath(), TerminalType.MOBILE, ChosenFormList.getNewInstance(), false, false)) {
            // 编译前端工程：面向Mobile设备
            generateAndCompileFrontendProject(context.getProjectPath(), TerminalType.MOBILE, false);
        }
    }

    /**
     * 前端工程的编译分为四步：预编译、将表单元数据(.frm)解析成 JSON、基于 JSON 生成前端代码（如Angular、Vue等）、将前端代码构建成 JS
     *
     * @param projectPath 待编译工程路径
     */
    private void generateAndCompileFrontendProject(String projectPath, TerminalType terminalType, boolean isJieXiForm) {
        // vue表单独立运行场景
        String runMetadataId = "";
        if (PublishServiceContextHolder.getCurrentContext().isPresent()) {
            PublishServiceContext publishServiceContext = PublishServiceContextHolder.getCurrentContext().get();
            runMetadataId = publishServiceContext.getWebParam().getRunFormMetadataId();
        }
        if (!StringUtility.isNullOrEmpty(runMetadataId)) {
            MetadataGetterParameter metadataGetterParameter = MetadataGetterParameter.getNewInstance(runMetadataId, projectPath, MetadataTypeEnum.Frm);
            GspMetadata vueFormMetadata = MetadataUtility.getInstance().getMetadataWithDesign(metadataGetterParameter);
            ResolveFormMetadataItem item = new ResolveFormMetadataItem();
            item.setGspMetadata(vueFormMetadata);
            item.setFormDynamicTag(false);
            item.setForceDynamicForm(false);
            List<ResolveFormMetadataItem> vueForm = new ArrayList<>();
            vueForm.add(item);
            List<String> relatedVueFormMetadataIds = this.getRelatedVueFormMetadataIds(vueFormMetadata);
            this.addResolveForms(relatedVueFormMetadataIds, projectPath, vueForm);

            resolveVueForm(projectPath, vueForm);
            return;
        }
        // 执行前端工程代码生成 如果生成失败 那么不进行编译动作
        if (generateFrontendProject(projectPath, terminalType)) {
            return;
        }

        // 将前端代码构建成 JS
        buildFrontendProject(projectPath, terminalType);
    }

    /**
     * 生成前端工程代码
     *
     * @param projectPath
     * @param terminalType
     * @return
     */
    private boolean generateFrontendProject(String projectPath, TerminalType terminalType) {

        boolean isGenerateNotSuccess = false;
        // 1. 预编译(编译前检测)
        List<GspMetadata> formMetadataInCurentProjectList = FrontendProjectUtility.getFormMetadataList(projectPath, terminalType);
        if (ListUtility.isEmpty(formMetadataInCurentProjectList)) {
            return true;
        }

        ChangeDetectContext changeDetectContext = new ChangeDetectContext();
        changeDetectContext.setProjectPath(projectPath);
        changeDetectContext.setTerminalType(terminalType);
        WebLogger.Instance.info("Web生成变更检测开始执行，对应工程路径为：" + projectPath);
//        boolean needGenerate = false;
        // 执行元数据变更检测
        ChangeDetectExecuteResult compileExecuteResult = ChangeDetectExecuteManager.execute(ChangeDetectExecuteType.Generate, changeDetectContext);
        // 如果元数据检测未发生变更 那么检测是否需要执行npm包更新
        // 之所以如此是因为如果元数据未发生变更，但jit发生变更，可能会新增对应属性
//        if (compileExecuteResult.isAllPass()) {
//            NpmInstallGlobalChecker.NpmInstallGlobalCheckResult checkResult = NpmInstallGlobalChecker.check(false);
//            needGenerate = checkResult.isNeedInstall();
//            if (needGenerate) {
//                WebLogger.Instance.info("全局包版本发生变更，执行Jit生成操作");
//            }
//        } else {
//            needGenerate = true;
//        }

        ResolveFormMetadataList formMetataList = FormMetadataManager.getFormMetadataList(projectPath, terminalType, ChosenFormList.getNewInstance());
        // 不存在表单，将其认定为非前端工程
        if (formMetataList.isEmpty()) {
            WebLogger.Instance.info("Debug_FrontendProjectCompiler_CompileFrontendProject: Current project is not a frontend project or has no forms!");
            return true;
        }
        List<ResolveFormMetadataItem> vueForm = new ArrayList<>();
        List<ResolveFormMetadataItem> angularForm = new ArrayList<>();

        formMetataList.getResolveFormMetadataItemList().forEach(form -> {
            GspMetadata formMd = form.getGspMetadata();
            MetadataProperties properties = formMd.getProperties();
            FrameWorkTypeEnum frameWorkTypeEnum = null;
            if (properties != null) {
                frameWorkTypeEnum = properties.getFramework();
            }
            if (frameWorkTypeEnum != null) {
                switch (frameWorkTypeEnum) {
                    case Vue:
                        vueForm.add(form);
                        break;
                    case Angular:
                        angularForm.add(form);
                        break;
                    default:
                        angularForm.add(form);
                        break;
                }
            } else {
                // 兼容老表单场景
                angularForm.add(form);
            }
        });

        // 只有angular表单会生成代码
        if (ListUtility.isEmpty(angularForm)) {
            isGenerateNotSuccess = true;
        }

        // Vue表单暂时不参与元数据变更检测
        if (!ListUtility.isEmpty(vueForm)) {
            resolveVueForm(projectPath, vueForm);
        }

        // 生成前编译检查
        if (!compileExecuteResult.isAllPass()) {
            WebLogger.Instance.info(compileExecuteResult.getUnPassReason());
            if (!ListUtility.isEmpty(angularForm)) {
                ResolveFormMetadataList angularResolveFormMetadataList = ResolveFormMetadataList.getNewInstance();
                angularResolveFormMetadataList.addAll(angularForm);
                // 2. 解析表单元数据
                FormMetadataResolver.resolveFormMetadatas(projectPath, angularResolveFormMetadataList, terminalType);
                // 3. 基于 JSON 生成前端代码
                WebLogger.Instance.info("开始执行Jit", FrontendProjectCompiler.class.getName());
                FrontendProjectGenerate.generateFrontendProject(projectPath, terminalType);

                // 生成完毕 执行元数据变更回写
                ChangeDetectExecuteManager.updateChangeset(ChangeDetectExecuteType.Generate, changeDetectContext);
            }
        } else {
            WebLogger.Instance.info("Web生成变更检测，未发生变更，不执行前端代码生成。对应工程路径为：" + projectPath);
        }
        return isGenerateNotSuccess;
    }

    private static void resolveVueForm(String projectPath, List<ResolveFormMetadataItem> vueForm) {
        ResolveFormMetadataList vueFormMetadataList =  ResolveFormMetadataList.getNewInstance();
        vueFormMetadataList.addAll(vueForm);
        // 2. 解析表单元数据
        FormMetadataResolver.resolveFormMetadatas(projectPath, vueFormMetadataList, TerminalType.VUE);
        // 3. 基于 JSON 生成前端代码
        WebLogger.Instance.info("开始执行Vue表单代码解析", FrontendProjectCompiler.class.getName());
        FrontendProjectGenerate.generateVueFrontendProject(projectPath, TerminalType.VUE);
    }

    /**
     * 构建前端工程
     */
    public final void buildFrontendProject(String projectPath, boolean isJieXiForm) {
        if (FormMetadataManager.checkFormMetadataExists(projectPath, TerminalType.PC, ChosenFormList.getNewInstance(), isJieXiForm, true)) {
            buildFrontendProject(projectPath, TerminalType.PC);
        }
        if (FormMetadataManager.checkFormMetadataExists(projectPath, TerminalType.MOBILE, ChosenFormList.getNewInstance(), isJieXiForm, true)) {
            buildFrontendProject(projectPath, TerminalType.MOBILE);
        }
    }

    /**
     * 构建前端工程
     */
    public final void buildFrontendProject(String projectPath, TerminalType terminalType) {
        // 增加编译前变更检测
        WebLogger.Instance.info("开始执行编译前变更检测，对应工程路径为：" + projectPath + ",表单类型:" + terminalType.getFormName());

        ChangeDetectContext changeDetectContext = new ChangeDetectContext();
        changeDetectContext.setProjectPath(projectPath);
        changeDetectContext.setTerminalType(terminalType);
        ChangeDetectExecuteResult compileExecuteResult = ChangeDetectExecuteManager.execute(ChangeDetectExecuteType.Compile, changeDetectContext);
        if (compileExecuteResult.isAllPass()) {
            // 如果未检测完毕  那么不进行编译
            WebLogger.Instance.info("Web编译变更检测，未发生任何变更，不执行代码编译。对应工程路径为：" + projectPath + ",表单类型：" + terminalType.getFormName());
        } else {
            WebLogger.Instance.info(compileExecuteResult.getUnPassReason());

            JITEngineManager.buildFrontendProject(projectPath, ExecuteEnvironment.Design, terminalType);

            // 执行元数据变更回写
            ChangeDetectExecuteManager.updateChangeset(ChangeDetectExecuteType.Compile, changeDetectContext);
        }
    }

    /**
     * 使用Babel构建前端工程
     *
     * @param projectPath
     */
    public final void buildFrontendProjectForBabel(String projectPath) {
        JITEngineManager.buildFrontendProjectForBabel(projectPath);
    }

    private void addResolveForms(List<String> metadataIds, String projectPath, List<ResolveFormMetadataItem> resolveFormMetadataItems) {
        metadataIds.forEach(metadataId -> {
            MetadataGetterParameter metadataGetterParameter = MetadataGetterParameter.getNewInstance(metadataId, projectPath, MetadataTypeEnum.Frm);
            GspMetadata vueFormMetadata = MetadataUtility.getInstance().getMetadataWithDesign(metadataGetterParameter);
            ResolveFormMetadataItem item = new ResolveFormMetadataItem();
            item.setGspMetadata(vueFormMetadata);
            item.setFormDynamicTag(false);
            item.setForceDynamicForm(false);
            resolveFormMetadataItems.add(item);
        });
    }

    private List<String> getRelatedVueFormMetadataIds(GspMetadata formMetadata) {
        List<String> result = new ArrayList<>();
        String baseFormRelativePath = formMetadata.getRelativePath();
        List<JsonNode> externalComponents = FormMetadataManager.getVueExternalComponents(formMetadata);
        externalComponents.forEach(externalComponent -> {
            JsonNode relativePath = externalComponent.get("relativePath");
            JsonNode id = externalComponent.get("id");
            if(relativePath == null || id == null){
                return;
            }
            String relativePathString = relativePath.asText();
            String idString = id.asText();
            if(StringUtils.isEmpty(relativePathString) || !relativePathString.equals(baseFormRelativePath)){
                return;
            }

            if (StringUtils.isNotEmpty(idString)) {
                result.add(idString);
            }
        });

        return result;
    }

}
